﻿using System;
using BitTorrent.Extentions;
using BitTorrent.Bencode.Collections;
using System.IO;
using BitTorrent.Bencode;
using System.Diagnostics.Contracts;
using BitTorrent.File.Exceptions;
using System.Collections.Generic;
using System.Linq;

namespace BitTorrent.File
{
    public class MetaFile
    {
        private readonly Dictionary _dictionary;

        /// <summary>
        /// Initializes a new instance of the File class.
        /// </summary>
        public MetaFile() {
            UId = -1;
            CreationDate = DateTime.MinValue;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="MetaFile"/> class.
        /// </summary>
        /// <param name="path">The path to the torrent file.</param>
        public MetaFile(string path) {
            _dictionary = GetDictonaryFromFile(path);

            Announce = GetAnnounce(_dictionary);
            CreationDate = GetCreationDate(_dictionary);
            CreatedBy = _dictionary.GetOptionalString("created by");
            Encoding = _dictionary.GetOptionalString("encoding");
            Commment = _dictionary.GetOptionalString("comment");
            Info = GetInfo(_dictionary);
            UId = _dictionary.GetOptionalInteger("uid");
        }

        #region Properties

        /// <summary>
        /// The announce URL of the tracker.
        /// </summary>
        /// <remarks>
        /// This supports the Multitracker Metadata Extension found in BEP 12
        /// </remarks>
        public IEnumerable<IEnumerable<Uri>> Announce { get; set; } 

        /// <summary>
        /// The creation time of the torrent.
        /// This property is optional if it doese not exist it Returns DateTime.MinValue.
        /// </summary>
        public DateTime CreationDate { get; set; }

        /// <summary>
        /// Name and version of the program used to create the torrent file.
        /// This property is optional and may return an empty string if not available.
        /// </summary>
        public string CreatedBy { get; set; }

        /// <summary>
        /// Textual comments from the author.
        /// This property is optional and may return an empty string if not available.
        /// </summary>
        public string Commment { get; set; }

        public long UId { get; set; }

        /// <summary>
        /// Information describing the file(s) of the torrent
        /// </summary>
        public Info Info { get; private set; }

        // TODO (fen): At this time I'm not using the encoding for anything 

        public string Encoding { get; set; }

        #endregion

        public void Save(string path) {
            var dictionary = new Dictionary();

            SaveAnnouceUri(dictionary);

            DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0);
            if (CreationDate != DateTime.MinValue || CreationDate > epoch) {
                TimeSpan span = (CreationDate - epoch);
                dictionary.Add((ByteString)"creation date", new Integer { Value = (long)span.TotalSeconds });
            }

            if (!string.IsNullOrEmpty(CreatedBy)) {
                dictionary.Add((ByteString)"created by", new ByteString { String = CreatedBy });
            }

            if (!string.IsNullOrEmpty(Commment)) {
                dictionary.Add((ByteString)"comment", new ByteString { String = Commment });
            }

            if (!string.IsNullOrEmpty(Encoding)) {
                dictionary.Add((ByteString)"encoding", new ByteString { String = Encoding });
            }

            dictionary.Add((ByteString)"info", Info.GetDictionary());

            if (UId != -1) {
                dictionary.Add((ByteString)"uid", new Integer { Value = UId });
            }

            using (var fileStream = System.IO.File.Create(path)) {
                byte[] buffer = Bencode.BencodeFactory.Encode(dictionary);
                fileStream.Write(buffer, 0, buffer.Length);
            }
        }

        #region Private

        private void SaveAnnouceUri(Dictionary dictionary) {
            // if we only have one announce uri in total we are not using the BEP 12 extention
            if (Announce.Count() == 1 && Announce.First().Count() == 1) {
                Uri uri = Announce.First().First();

                dictionary.Add((ByteString)"announce", new ByteString { String = uri.ToString() });
            }
            else {
                List outerList = new List();
                foreach (var annouceUris in Announce) {
                    List innerList = new List();
                    foreach (Uri uri in annouceUris) {
                        innerList.Add(new ByteString { String = uri.ToString() });
                    }
                    outerList.Add(innerList);
                }
                dictionary.Add((ByteString)"announce-list", outerList);
            }
        }

        private static Dictionary GetDictonaryFromFile(string path) {
            if (!System.IO.File.Exists(path)) {
                throw new FileNotFoundException(path);
            }

            byte[] bytes = System.IO.File.ReadAllBytes(path);

            var dictonary = BencodeFactory.Decode(bytes) as Dictionary;
            if (dictonary == null) {
                throw new TorrentFileException("Not starting with a Bencode.Diconary");
            }

            return dictonary;
        }

        private static IEnumerable<IEnumerable<Uri>> GetAnnounce(Dictionary dictionary) {
            // If the 'announce-list' key is present, we will ignore the 'announce' key and 
            // only return the 'announce-list' as specified in the BEP 12.
            if (dictionary.ContainsKey("announce-list")) {
                var annouceList = dictionary["announce-list"] as List;
                if (annouceList == null) {
                    throw new TorrentFileException("Announce list have to be an bencode list");
                }
                var resultList = new List<IEnumerable<Uri>>();
                foreach (var obj in annouceList) {
                    var list = obj as List;
                    if (list == null) {
                        throw new TorrentFileException("Announce list have to be an becode list of list");
                    }
                    var innerResultList = new List<Uri>();
                    foreach (var uri in list) {
                        var announceUri = uri as ByteString;
                        if (announceUri == null) {
                            throw new TorrentFileException("Announce list have to be an becode list of list of byte strings");
                        }
                        innerResultList.Add(new Uri(announceUri.String));
                    }
                    resultList.Add(innerResultList);
                }
                return resultList;
            }
            else {
                if (!dictionary.ContainsKey("announce")) {
                    throw new TorrentFileException("There is no announce uri set for the torrent file");
                }

                var announce = dictionary["announce"] as ByteString;
                if (announce == null) {
                    throw new TorrentFileException("The value representing announce was not the expected ByteString");
                }

                var resultList = new List<IEnumerable<Uri>>();
                var innerResultList = new List<Uri> {new Uri(announce.String)};
                resultList.Add(innerResultList);
                return resultList;
            }
        }

        private static DateTime GetCreationDate(Dictionary dictionary) {
            if (!dictionary.ContainsKey("creation date")) {
                return DateTime.MinValue;
            }

            var benObj = dictionary["creation date"];
            // I will not throw an exception because creation date is optional
            if (!(benObj is Integer)) {
                return DateTime.MinValue;
            }

            long seconds = ((Integer)benObj).Value;

            // Convert from UNIX epoch format to DateTime
            var result = new DateTime(1970, 1, 1, 0, 0, 0, 0);
            
            return result.AddSeconds(seconds);
        }

        private static Info GetInfo(Dictionary dictionary) {
            Contract.Requires(dictionary != null);
            if (!dictionary.ContainsKey("info")) {
                throw new TorrentFileException("There was no info Bencode.Dictonary in the torrent file");
            }

            var info = dictionary["info"] as Dictionary;
            if (info == null) {
                throw new TorrentFileException("The value representing info was not the expected Bencode.Dictonary");
            }
            
            if (MultiFile.IsMultiFileInfo(info)) {
                return new MultiFile(info);
            }
            
            return new SingelFile(info);
        }

        #endregion
    }
}